Skip to content

feat!: complete full Zeo device support#871

Draft
NOisi-x wants to merge 20 commits into
Python-roborock:mainfrom
NOisi-x:main
Draft

feat!: complete full Zeo device support#871
NOisi-x wants to merge 20 commits into
Python-roborock:mainfrom
NOisi-x:main

Conversation

@NOisi-x

@NOisi-x NOisi-x commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR completes Zeo (washing machine) device support in python-roborock.

The library already had foundational A01 protocol support for Zeo devices, but several critical gaps prevented practical use: the start command didn't work, roughly half of the DP IDs were undefined, several device states and enum classes were missing, and most data points had no value converters. This PR closes those gaps, bringing the DP and enum definitions in line with the Roborock Washer app protocol.

With these changes, Zeo devices support the full range of operations defined by the protocol: start/stop/pause/shutdown, mode and program selection, temperature, rinse, spin, drying control, child lock, auto detergent/softener, UV sterilization, soak, silent mode, dry care, steam, ion deodorization, wash-dry linkage, and more.

Motivation

The library's existing Zeo set_value(START, 1) did not actually start the device. Through protocol analysis and comparison with the Roborock Washer HomeAssistant integration, three root causes were identified:

  1. QoS level: DP200 must be published at QoS 1; the default QoS 0 was rejected by the broker.
  2. Missing timestamp: A01 command payloads require a "t" (Unix timestamp) field.
  3. Missing parameter bundle: The device firmware requires MODE and PROGRAM to be sent together with START in a single MQTT message.

Beyond the start command, RoborockZeoProtocol covered only 31 of 67 known DP IDs, zeo_code_mappings.py was missing 4 enum classes and several enum values, and ZEO_PROTOCOL_ENTRIES provided converters for only 16 of the 52 user-facing DPs — meaning query_values() silently returned None for most data points.

Changes

1. MQTT QoS — roborock/mqtt/roborock_session.py

Changed client.publish() to use qos=1 (at-least-once delivery).

2. A01 payload timestamp — roborock/protocols/a01_protocol.py

Added "t": int(time.time()) to the JSON payload structure in encode_mqtt_payload().

3. Combined start command — roborock/devices/traits/a01/__init__.py

ZeoApi.set_value(START, 1) now:

  • Queries the device for its current MODE, PROGRAM, TEMP, RINSE_TIMES, SPIN_LEVEL, and DRYING_MODE
  • Bundles the returned values with {START: 1} into a single MQTT message
  • Falls back to MODE=1, PROGRAM=1 if the query fails

4. Extended DP ID enum — roborock/roborock_message.py

RoborockZeoProtocol expanded from 31 to 67 entries, now covering all 52 user-facing DPs (200-266) plus meta DPs. Newly added: dirt detection (215-216), UV light (228), soak (233), total time (234), smart hosting (235-238), silent mode (240-242), dry care mode (244), wash-dry linkage (255), drying method (256), steam volume (257), ion deodorization (258), dryer start error (265), and more.

5. Enum updates — roborock/data/zeo/zeo_code_mappings.py

  • ZeoMode: added treatment = 4
  • ZeoState: expanded from 13 to 20 states
  • ZeoSoak: added very_max = 5
  • New enum classes: ZeoDryingMethod, ZeoSteamVolume, ZeoDryAndCare, ZeoDryerStartError

6. Value converters — roborock/devices/traits/a01/__init__.py

ZEO_PROTOCOL_ENTRIES expanded from 16 to 57 entries, providing typed converters for all user-facing DPs (200-266).

Backward Compatibility

All changes are additive or transparent:

  • ZeoApi.query_values() and ZeoApi.set_value() signatures unchanged
  • New DP IDs and enums are pure additions
  • QoS change applies to all MQTT publishes, matching the expected at-least-once delivery
  • Start command query adds ~1-2s latency; all other set_value calls unaffected

Known Limitation

sent_decoded_command uses a strict "all requested DPs must respond" completion condition. Since A01 devices do not expose a capability probing mechanism, there is currently no clean way to determine which optional DPs (e.g., SOAK, STEAM_VOLUME) a given device supports before querying them.

As a temporary workaround, the start command queries and bundles only the following 6 core DPs, which has been verified to successfully start devices on physical Zeo hardware:

DP Name Purpose
204 MODE Wash mode (e.g. wash, wash-and-dry, dry)
205 PROGRAM Wash program (e.g. standard, quick, wool)
207 TEMP Water temperature
208 RINSE_TIMES Number of rinse cycles
209 SPIN_LEVEL Spin speed
210 DRYING_MODE Drying mode (e.g. quick, iron, store)

These are bundled with {START: 1} and sent as a single MQTT message. The device applies its internally-stored values for any parameters not explicitly included.

A better long-term solution for dynamically detecting device capabilities is welcome — contributions or ideas in this area are appreciated.

Testing

  • set_value(START, 1) verified working on physical Zeo One and Zeo M1S devices
  • set_value for non-START DPs (mode, program, temperature, etc.) continues to work as before
  • Coordinator polling and DPS push updates unaffected
  • All changes pass linting (ruff, mypy)

Acknowledgments

This PR draws heavily on the excellent reverse-engineering work done in ndwzy/roborock_washer_ha. The A01 protocol structure, DP definitions, enum mappings, and the combined start command behavior were all cross-referenced against that project's implementation. The DP ID list (200-266), state enum values, and device error codes were verified against the Roborock app bundle extracted and analyzed by that project.


Closes #833

@Lash-L

Lash-L commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Hey @NOisi-x something about your changes seem to have broken tests. Tests are running indefinitely, can you take a look?

@NOisi-x

NOisi-x commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Hey @NOisi-x something about your changes seem to have broken tests. Tests are running indefinitely, can you take a look?

Hi, @Lash-L ,I've updated the test files and all tests are now passing. Thanks!

CI Failure Analysis and Fix Summary

Apologies for the broken CI — I'm a hobbyist with fairly limited programming experience and I overlooked the fact that the source changes would affect the existing test suite. Lesson learned for next time. Below is an analysis of what went wrong and what was changed to fix it.

Root Causes

The CI test suite failed for three reasons:

  1. Payload format mismatch: encode_mqtt_payload() now includes a "t" field. 4 unit test assertions used exact-match checks.

  2. FakeChannel signature: MqttChannel.publish() gained a qos parameter. FakeChannel._publish did not accept it.

  3. Time-dependent snapshot: The "t" field value comes from time.time(), making the A01 e2e snapshot non-deterministic across CI runs.

Changes Made

tests/devices/traits/a01/test_init.py — 4 assertions updated

Replaced exact-match with key-check: payload_data["dps"] == {...} + assert "t" in payload_data.

roborock/testing/channel.py — 1 signature update

FakeChannel._publish now accepts qos: int = 0 to match the updated MqttChannel.publish signature.

tests/e2e/test_device_manager.py — time frozen for A01 test

Added from freezegun import freeze_time and decorated test_a01_device with @freeze_time("2025-01-20T12:00:00") to ensure deterministic MQTT payloads across CI runs. Snapshot regenerated via --snapshot-update.

roborock/devices/traits/a01/__init__.py — 1 import fix

Removed stray blank line between RoborockException and roborock.data imports (ruff/isort ordering).

Tests Not Affected

  • tests/protocols/test_a01_protocol.pydecode_rpc_response() ignores the "t" field
  • tests/e2e/test_mqtt_session.py — no client-side publish affected
  • tests/devices/test_a01_traits.py — patches send_decoded_command entirely
  • All V1/B01/Q7/Q10/Dyad tests — no changes needed

@allenporter allenporter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic contribution, thank you so much for the improvements here!

Comment thread roborock/devices/transport/mqtt_channel.py Outdated
Comment thread roborock/data/zeo/zeo_code_mappings.py
Comment thread roborock/devices/traits/a01/__init__.py
Comment thread roborock/devices/traits/a01/__init__.py Outdated
Comment thread tests/e2e/test_device_manager.py Outdated
@NOisi-x NOisi-x changed the title feat: complete full Zeo device support feat!: complete full Zeo device support Jul 13, 2026
Comment thread roborock/mqtt/session.py Outdated
Comment thread roborock/devices/traits/a01/__init__.py
Comment thread roborock/data/zeo/zeo_code_mappings.py
@NOisi-x NOisi-x requested a review from allenporter July 13, 2026 16:21
@NOisi-x

NOisi-x commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@allenporter Thank you very much for your code review. As I spent more time analyzing the files obtained through reverse engineering, I made more exciting discoveries. I'll temporarily convert this PR to draft status and will continue to refine it. Please review it again once it's ready—thanks again!

@NOisi-x NOisi-x marked this pull request as draft July 15, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature/Bug] Zeo Washing Machine (M1S Ultra): Deep Sleep Wakeup Failure & Missing DP Mappings

3 participants